import { isIP } from 'net';
import { URL } from 'url';
import { pathOr } from 'ramda';
import tld from 'tldjs';

import { warnEventId } from '../../../common/warnId';
import errorCodesBuilder from '../../helpers/errorCodesBuilder';
import monitorTaskRequest from './monitorTaskRequest';

const errorCodes = errorCodesBuilder();

export function subDomainsRespParser(resp) {
	return resp.map(({ host, point }) => ({
		host,
		point: point === '-' ? '' : point,
	}));
}

export function ipPortsRespParser(resp) {
	return resp.map(({ ip, data }) => ({ ip, ports: data }));
}

export function bindDomainsRespParser(resp) {
	return resp.map(x => ({ ip: x.ip, domanis: x.data }));
}

/**
 * 子域名监测 ---> IP 端口扫描
 *             \-> IP 反查域名
 */
export default (config, server) => {
	return async queueTask => {
		const {
			app: {
				model: { Asset },
			},
		} = server;

		queueTask.update({ ...queueTask.data, ...config });
		const { target, sourceIp } = queueTask.data;
		const domain = new URL(target).hostname;
		server.log.debug({ target }, '资产变动监测任务开始执行');
		async function passTask() {
			const pass = isIP(domain);
			if (pass) {
				return Promise.resolve([]);
			} else {
				try {
					const resp = await monitorTaskRequest({
						job: queueTask,
						queueTaskIdField: 'subdomainQueueTaskId',
						api: config.subdomaindiscoverApi,
						checkInterval: config.checkInterval,
						payload: {
							domain: tld.getDomain(domain),
						},
					});
					return (resp?.data || [])
						.filter(x => (x?.host || '').endsWith(`.${domain}`))
						.map(({ host, ip, cname }) => ({
							host,
							cname,
							point: ip === '-' ? '' : ip,
						}));
				} catch (err) {
					const error = errorCodes['[02]资产监测Worker子域名扫描失败'].from(
						err
					);
					server.log.error(
						{ error, target, api: config.portscanApi },
						error.message
					);
					throw error;
				}
			}
		}
		const [subDomains, ipPorts] = await Promise.allSettled([
			passTask(),
			monitorTaskRequest({
				job: queueTask,
				queueTaskIdField: 'portQueueTaskId',
				api: config.portscanApi,
				checkInterval: config.checkInterval,
				payload: { ip: sourceIp },
			})
				.then(({ ip, data }) => [{ ip, ports: data }])
				.catch(err => {
					const error = errorCodes['[02]资产监测Worker端口扫描任务失败'].from(
						err
					);
					server.log.error(
						{ error, target, api: config.portscanApi },
						error.message
					);
					throw error;
				}),
		]);
		queueTask.progress(70);
		let ipBindDomains = [];
		if (ipPorts.status === 'fulfilled') {
			const ip = pathOr('', [0, 'ip'], ipPorts);
			if (isIP(ip) > 0) {
				ipBindDomains = await monitorTaskRequest({
					job: queueTask,
					queueTaskIdField: 'ip2domainQueueTaskId',
					api: config.ip2domainApi,
					checkInterval: config.checkInterval,
					payload: {
						ip: domain,
					},
				})
					.then(resp => [{ ip: resp.ip, domanis: resp.data }])
					.catch(err => {
						const error = errorCodes[
							'[02]资产监测Worker绑定域名扫描任务失败'
						].from(err);
						server.log.error(
							{
								error,
								target,
								api: config.ip2domainApi,
							},
							error.message
						);
						return [];
					});
			}
		}

		const ret = {
			ipBindDomains,
			subDomains: subDomains.status === 'fulfilled' ? subDomains.value : [],
			ipPorts: ipPorts.status === 'fulfilled' ? ipPorts.value : [],
		};
		try {
			await Asset.updateOne(
				{ _id: queueTask.data.assetId },
				{
					$set: { ...ret, updateTime: Date.now() },
				}
			);
		} catch (err) {
			const error = errorCodes['[02]资产监测Worker更新资产失败'].from(err);
			server.log.error({ error, target }, error.rawMessage);
		}

		queueTask.progress(100);

		ret.warnings = await warningsCheck({ server, queueTask, result: ret });

		ret.stats = {
			subdomainCount: 0,
			ipCount: 0,
			portCount: 0,
			ipBindDomainCount: 0,
			addSubdomainCount: 0,
			addIpCount: 0,
			addPortCount: 0,
			addIpBindDomainCount: 0,
			removeSubdomainCount: 0,
			removeIpCount: 0,
			removePortCount: 0,
			removeIpBindDomainCount: 0,
			changePortServiceCount: 0,
		};
		ret.stats.subdomainCount = ret.subDomains.length;
		ret.stats.ipCount = ret.ipPorts.length;
		ret.stats.portCount = ret.ipPorts.reduce(
			(count, x) => count + x.ports?.length || 0,
			0
		);
		ret.stats.ipBindDomainCount = ret.ipBindDomains.reduce((set, record) => {
			set.add(...record.domains.forEach(x => set.add(x.domain)));
			return set;
		}, new Set()).size;
		ret.warnings.reduce((obj, x) => {
			let key;
			if (x.category === 'portService') {
				key = 'changePortServiceCount';
			} else {
				const category = x.category[0].toUpperCase() + x.category.slice(1);
				const op = x.detail.op;
				key = `${op}${category}Count`;
			}
			obj[key] += 1;

			return obj;
		}, ret.stats);

		return ret;
	};
};

async function warningsCheck({
	server,
	result: { subDomains, ipPorts },
	queueTask,
}) {
	const {
		app: {
			model: { Asset },
		},
	} = server;

	let asset;
	try {
		asset = await Asset.findOne({ _id: queueTask.data.refId })
			.populate('resultId')
			.lean();
	} catch (err) {
		const error = errorCodes['[02]监测结果处理：关联Mongo记录查询失败'].from(
			err
		);
		server.log.error({ error }, error.rawMessage);
		throw error;
	}

	if (!asset?.resultId?.result) return [];
	const prevResult = asset.resultId.result;

	const warnings = [];

	const subdomainChanges = changesCompare(
		pathOr([], ['subDomains'], prevResult).map(x => x.host),
		subDomains.map(x => x.host)
	);
	subdomainChanges.add.forEach(domain => {
		warnings.push({
			id: warnEventId({
				taskType: 'asset',
				id: 'subdomain',
				affects: domain,
			}),
			level: 1,
			title: `[${domain}]子域名上线`,
			category: 'subdomain',
			affects: domain,
			detail: { op: 'add' },
		});
	});
	subdomainChanges.remove.forEach(domain => {
		warnings.push({
			id: warnEventId({
				taskType: 'asset',
				id: 'subdomain',
				affects: domain,
			}),
			level: 1,
			title: `[${domain}]子域名下线`,
			category: 'subdomain',
			affects: domain,
			detail: { op: 'remove' },
		});
	});

	const portChanges = changesCompare(
		pathOr([], ['ipPorts', 0, 'ports'], prevResult).map(x => x.port),
		pathOr([], [0, 'ports'], ipPorts).map(x => x.port)
	);
	portChanges.add.forEach(port => {
		warnings.push({
			id: warnEventId({ taskType: 'asset', id: 'port', affects: port }),
			level: 1,
			title: `[${port}]端口开放`,
			category: 'port',
			affects: port,
			detail: { op: 'add' },
		});
	});
	portChanges.remove.forEach(port => {
		warnings.push({
			id: warnEventId({ taskType: 'asset', id: 'port', affects: port }),
			level: 1,
			title: `[${port}]端口关闭`,
			category: 'port',
			affects: port,
			detail: { op: 'remove' },
		});
	});

	if (portChanges.stay.length) {
		const currentPortServiceMap = pathOr([], [0, 'ports'], ipPorts).reduce(
			(obj, x) => {
				obj[x.port] = x.service || '未知服务';
				return obj;
			},
			{}
		);
		const prevPortServiceMap = pathOr(
			[],
			['ipPorts', 0, 'ports'],
			prevResult
		).reduce((obj, x) => {
			obj[x.port] = x.service || '未知服务';
			return obj;
		}, {});
		portChanges.stay.forEach(port => {
			if (prevPortServiceMap[port] === currentPortServiceMap[port]) return;
			warnings.push({
				id: warnEventId({
					taskType: 'asset',
					id: 'portService',
					affects: port,
				}),
				level: 1,
				title: `端口服务${port}(${prevPortServiceMap[port]})调整为${port}(${currentPortServiceMap[port]})`,
				category: 'portService',
				affects: port,
				detail: {
					prev: prevPortServiceMap[port],
					current: currentPortServiceMap[port],
				},
			});
		});
	}

	return warnings;
}

export function changesCompare(prev, next) {
	const removeMap = prev.reduce((obj, x) => {
		obj[x] = true;
		return obj;
	}, {});
	const nextMap = next.reduce((obj, x) => {
		obj[x] = true;
		return obj;
	}, {});

	const addMap = {};
	const stayMap = {};
	Object.keys(nextMap).forEach(key => {
		if (!removeMap[key]) {
			addMap[key] = true;
		} else {
			stayMap[key] = true;
		}
		delete removeMap[key];
	});

	return {
		add: Object.keys(addMap),
		remove: Object.keys(removeMap),
		stay: Object.keys(stayMap),
	};
}
